﻿using System;
using System.Collections.Generic;
using System.Text;
using llamaServer;
using dAmn;
using llAma.Backend.Interface;
using llAma.Interface;
using System.Net;
using System.Threading;

namespace llAma.Base
{
    public abstract class BackendBase : llamaCommonBase, IBackend
    {
        private delegate void clnDlg();
        protected BackendBase() : base() {
            //Begin the cleanup pump.
            CleanupThread.Invoke(new clnDlg(Cleanup));
        }
        DelayedExecution CleanupThread = new DelayedExecution("cleanup");
        #region Member Variables
        //Constants
        //const int connHardLimit = 200;
        //const int loginHardLimit = 40;
        protected int loginHardLimit
        {
            get
            {
                int ret = Server.Config["connections"]["max_same_login"];
                if (ret <= 0)
                    loginHardLimit = 20;
                else
                    return ret;
                return 20;
            }
            set
            {
                Server.Config["connections"]["max_same_login"] = value;
                Server.SaveConfig();
            }
        }
        protected int connHardLimit
        {
            get
            {
                int ret = Server.Config["connections"]["max_same_ip"];
                if (ret <= 0)
                    connHardLimit = 200;
                else
                    return ret;
                return 200;

            }
            set
            {
                Server.Config["connections"]["max_same_ip"] = value;
                Server.SaveConfig();
            }
        }

        //Instance Menbers
        Dictionary<IPAddress, List<IConnection>> userlessConnections = new Dictionary<IPAddress, List<IConnection>>();
        Dictionary<string, List<IConnection>> userConnections = new Dictionary<string, List<IConnection>>();
        IGpc mGpcHandler = null;
        ManualResetEvent loginSignal = new ManualResetEvent(true);

        //Static members

        #endregion
        #region Abstracts
        protected abstract IChannel OnChannelRequest(string full_namespace);
        protected abstract IUserData OnRequestAuthentication(string user, string token, Guid uuid);
        #endregion
        #region Virtuals
        protected virtual void OnHandleDisconnect(Packet p, object sender)
        {
            IConnection conn = (IConnection)sender;
            conn.Drop("quit");
        }
        protected virtual IGpc OnGPCRequest()
        {
            return null;
        }
        protected virtual void OnHandleChannelPacket(Packet p, IConnection conn)
        {
            //sender always implements IConnection
            //IChannel channel = LookupChannel(p.param);
            IChannel channel = ((IBackend)this).LookupChannel(p.param);
            if (channel == null)
                SendErrorPacketToClient(conn, p, "bad namespace");
            else
                channel.HandlePacket(p, conn);
        }
        protected virtual void OnLogMessage(string message)
        {

        }
        protected virtual void OnPacket(Packet p, object sender)
        {
            this.ExecutePacketEvents(p, sender);
        }
        #endregion

        #region Interface implementation
        IUserData IBackend.OneWayAuthenticate(string user, string token, Guid uuid)
        {
            return OnRequestAuthentication(user, token, uuid);
        }

        IGpc IBackend.GpcHandler
        {
            get
            {
                if (mGpcHandler == null)
                    mGpcHandler = OnGPCRequest();
                return mGpcHandler;
            }
        }

        void IBackend.LogMessage(string message)
        {
            OnLogMessage(message);
        }



        IChannel IBackend.LookupChannel(string full_namespace)
        {
            return OnChannelRequest(full_namespace);
        }
        void IHandlePackets.HandlePacket(Packet p, IConnection sender)
        {
            this.OnPacket(p, sender);
        }

        void IBackend.OnNewConnection(IConnection c)
        {
            Interlocked.Increment(ref mTotalConnections);
            if (!userlessConnections.ContainsKey(c.RemoteAddress))
                userlessConnections[c.RemoteAddress] = new List<IConnection>();
            userlessConnections[c.RemoteAddress].Add(c);
            c.OnDisconnect += new EventHandler<ClientEventArgs>(c_OnDisconnect);
            if (GetConnectionCount(c.RemoteAddress) >= connHardLimit)
            {
                c.Drop("too many connections");
            }

        }
        protected void Cleanup()
        {
            //first iterate over the userless connections
            //Switch to low priority.
            Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
            while (true)
            {
                Thread.Sleep(30000); //Every 30 seconds.
                GC.Collect();
                IPAddress[] userlessKeys;
                lock (userlessConnections)
                {
                    Thread.CurrentThread.Priority = ThreadPriority.Highest;
                    userlessKeys = new IPAddress[userlessConnections.Keys.Count];
                    userlessConnections.Keys.CopyTo(userlessKeys, 0);
                    Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
                }
                foreach (IPAddress k in userlessKeys)
                {
                    if (!userlessConnections.ContainsKey(k)) continue;
                    List<IConnection> badConnections = new List<IConnection>();
                    if (userlessConnections[k].Count > 0)
                    {
                        lock (userlessConnections)
                        {
                            Thread.CurrentThread.Priority = ThreadPriority.Highest;
                            foreach (IConnection c in userlessConnections[k])
                            {
                                if (!c.Connected) badConnections.Add(c);
                            }
                            Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
                        }
                    }
                    Thread.Sleep(50);
                    if (badConnections.Count != 0)
                    {
                        lock (userlessConnections)
                        {
                            //Finally, Iteriate over the list and remove the dead connections.
                            if (badConnections.Count == userlessConnections[k].Count)
                            {
                                userlessConnections[k].Clear();
                            }
                            else
                            {
                                foreach (IConnection c in badConnections)
                                {
                                    userlessConnections[k].Remove(c);
                                }
                            }
                        }
                        //Sleep for a bit, Outside the lock only.
                        Thread.Sleep(50);
                    }
                }
                string[] userKeys;
                lock (userConnections)
                {
                    userKeys = new string[userConnections.Keys.Count];
                    userConnections.Keys.CopyTo(userKeys, 0);
                }
                foreach (string s in userKeys)
                {
                    if (!userConnections.ContainsKey(s)) continue;
                    List<IConnection> badConnections = new List<IConnection>();
                    if (userConnections[s].Count > 0)
                    {
                        lock (userConnections)
                        {
                            foreach (IConnection c in userConnections[s])
                            {
                                if (!c.Connected) badConnections.Add(c);
                            }
                        }
                    }
                    Thread.Sleep(50);
                    if (badConnections.Count != 0)
                    {
                        lock (userConnections)
                        {
                            //Finally, Iteriate over the list and remove the dead connections.
                            if (badConnections.Count == userConnections[s].Count)
                            {
                                userConnections[s].Clear();
                            }
                            else
                            {
                                foreach (IConnection c in badConnections)
                                {
                                    userConnections[s].Remove(c);
                                }
                            }
                        }
                        //Sleep for a bit, Outside the lock only.
                        Thread.Sleep(50);
                    }
                }
                GC.Collect();
            }
        }
        void c_OnDisconnect(object sender, ClientEventArgs e)
        {
            //Remove our attached event handler.
            e.client.OnDisconnect -= new EventHandler<ClientEventArgs>(c_OnDisconnect);
            //Lock both dicts, so nothing can access either until we are done.
            lock (userConnections)
            {
                lock (userlessConnections)
                {
                    if (userlessConnections.ContainsKey(e.client.RemoteAddress))
                    {
                        userlessConnections[e.client.RemoteAddress].Remove(e.client);
                    }
                    if (e.client.CurrentUser != null &&
                        userConnections.ContainsKey(e.client.CurrentUser.getInfo("username").ToLower()))
                    {
                        userConnections[e.client.CurrentUser.getInfo("username").ToLower()].Remove(e.client);
                    }
                }
            }
            Interlocked.Decrement(ref mTotalConnections);
        }
        #endregion

        #region Packet Handlers
        [PacketHandler("join")]
        protected void handleJoinPacket(Packet p, IConnection conn)
        {
            if (!conn.Connected)
                return;
            IChannel chan = ((IBackend)this).LookupChannel(p.param);
            if (chan == null)
            {
                SendErrorPacketToClient(conn, p, "bad namespace");
                return;
            }
            foreach (IConnection k in chan.Connections)
            {
                if (k.UUID == conn.UUID)
                {
                    SendErrorPacketToClient(conn, p, "already joined");
                    return;
                }
            }
            OnHandleChannelPacket(p, conn);
        }
        [PacketHandler("admin")]
        [PacketHandler("set")]
        [PacketHandler("ban")]
        [PacketHandler("unban")]
        [PacketHandler("kick")]
        [PacketHandler("promote")]
        [PacketHandler("deomote")]
        [PacketHandler("send")]
        [PacketHandler("part")]
        protected void handleRoomPacket(Packet p, IConnection conn)
        {
            if (!conn.Connected)
                return;
            IChannel chan = ((IBackend)this).LookupChannel(p.param);
            if (chan == null)
            {
                SendErrorPacketToClient(conn, p, "bad namespace");
                return;
            }
            bool inChannel = false;
            foreach (IConnection k in chan.Connections)
            {
                if (k.UUID == conn.UUID)
                {
                    inChannel = true;
                    break;
                }
            }
            if (!inChannel)
            {
                SendErrorPacketToClient(conn, p, "not joined");
                return;
            }
            OnHandleChannelPacket(p, conn);
        }
        [PacketHandler("get")]
        protected void handleGetPacket(Packet p, IConnection conn)
        {
            if (!p.param.StartsWith("login:"))
            {
                handleJoinPacket(p, conn);
            }
            string[] parts = p.param.Split(':');
            if (p.args["p"] != "info")
            {
                SendErrorPacketToClient(conn, p, "Invalid property requested.");
                return;
            }
            HandleWhoisPacket(parts[1], conn);
        }
        protected virtual void HandleWhoisPacket(string name, IConnection conn)
        {
            //Do nothing, This is up to the backend to implement.
        }
        //We can also handle disconnect here.
        [PacketHandler("disconnect")]
        protected void handleDisconnect(Packet p, IConnection conn)
        {
            if (!conn.Connected) return;
            OnHandleDisconnect(p, conn);
        }
        
        [PacketHandler("login")]
        protected void packetLoginHandler(Packet p, IConnection conn)
        {
            try
            {
                IUserData myUser = null;
                if (conn.CurrentUser != null)
                {
                    conn.Drop("bad command");
                }
                if (!p.args.ContainsKey("pk"))
                {
                    conn.Drop("no login"); return;
                }
                myUser = ((IBackend)this).OneWayAuthenticate(p.param, p.args["pk"], conn.UUID);
                if (myUser == null)
                {
                    SendErrorPacketToClient(conn, p, "authentication failed"); return;
                }
                //Check how many connections they have :B
                Packet toClient = Packet.EmptyPacket;
                toClient.cmd = "login";
                toClient.param = p.param;

                toClient.param = myUser.getInfo("username");
                if (GetConnectionCount(myUser.getInfo("username")) >= loginHardLimit)
                {
                    SendErrorPacketToClient(conn, toClient, "too many connections");
                    conn.Drop("too many connections");
                    return;
                }
                conn.CurrentUser = myUser;
                PermissionRule r = new PermissionRule("login", 0, Comparison.NEQ);
                if (!((IBackend)this).GpcHandler.Demand(r, myUser.getInfo("gpc")))
                {
                    toClient.args["e"] = "banned";
                    conn.Send(toClient.ToString());
                    conn.Drop("banned");
                    return;
                }
                toClient.args["e"] = "ok";
                //Some trickery here is needed.
                Packet newArgs = Packet.EmptyPacket;
                newArgs.cmd = string.Empty;
                newArgs.param = string.Empty;
                //TODO: populate newArgs with actual user data.
                newArgs.args["symbol"] = myUser.getInfo("symbol");
                newArgs.args["gpc"] = myUser.getInfo("gpc");
                newArgs.args["realname"] = myUser.getInfo("realname");
                newArgs.args["typename"] = myUser.getInfo("typename");
                newArgs.args["usericon"] = myUser.getInfo("usericon");
                toClient.body = newArgs.ToString();
                toClient.param = conn.CurrentUser.getInfo("username");
                //And we can now switch to the logged in state.
                conn.State = ConnectionState.STATE_LOGGEDIN;
                conn.Send(toClient);
                AddConnectionToUserPool(conn);
            }
            finally
            {
                loginSignal.Set();
            }
        }

        private void AddConnectionToUserPool(IConnection conn)
        {
            string uid = conn.CurrentUser.getInfo("username").ToLower();
            if (!userConnections.ContainsKey(uid))
            {
                lock (userConnections)
                {
                    if (!userConnections.ContainsKey(uid))
                    {
                        userConnections[uid] = new List<IConnection>();
                    }
                }
            }
            userConnections[uid].Add(conn);
        }
        private long mTotalConnections = 0;
        private int GetConnectionCount(IPAddress p)
        {
            int x = 0;
            List<IConnection> toRemove = new List<IConnection>();
            foreach (IConnection c in GetUserConnections(p))
            {
                if (c.Connected) x++;
                else toRemove.Add(c);
            }
            foreach (IConnection c in toRemove)
            {
                c.Drop("timed out");
            }
            return x;
            //return GetUserConnections(p).Length;
        }

        private int GetConnectionCount(string p)
        {
            List<IConnection> toRemove = new List<IConnection>();
            int x = 0;
            foreach (IConnection c in GetUserConnections(p))
            {
                if (c.Connected) x++;
                else toRemove.Add(c);
            }
            foreach (IConnection c in toRemove)
            {
                c.Drop("timed out");
            }
            return x;
        }
        protected IConnection[] GetUserConnections(IPAddress p)
        {
            if (!userlessConnections.ContainsKey(p))
                return new IConnection[0];
            else
                return userlessConnections[p].ToArray();
        }
        protected IConnection[] GetUserConnections(string p)
        {
            p = p.ToLower();
            if (!userConnections.ContainsKey(p))
                return new IConnection[0];
            else
                return userConnections[p].ToArray();
        }
        [PacketHandler("kill")]
        protected void OnKillPacket(Packet p, IConnection conn)
        {
            if(!((IBackend)this).GpcHandler.Demand(new PermissionRule("kill", 0, Comparison.NEQ), conn.CurrentUser.getInfo("gpc")))
            {
                return;
            }
            string[] parts = p.param.Split(':');
            IConnection[] conns = null;
            if(parts.Length != 2)
                return;
            switch (parts[0])
            {
                case "login":
                    conns = GetUserConnections(parts[1]);
                    if (conns.Length > 0)
                    {
                        if (!CheckKill(conn, conns[0]))
                        {
                            SendErrorPacketToClient(conn, p, "not privileged");
                            return;
                        }
                    }
                    break;
                case "chat": //IDK if dAmn can do this, But imho this is a good thing to have :B
                    IChannel target = this.OnChannelRequest(p.param);
                    if (target == null) return;
                    conns = target.Connections;
                    break;
                case "system":
                    //This is a bit trickier

                    break;
                default:
                    return;
            }
            //Don't waste time building the kill message if we don't have anyone to kill.
            if (conns.Length == 0) return;
            StringBuilder sb = new StringBuilder("killed");
            string msg = SanitizeUserInput(p.body);
            if (!string.IsNullOrEmpty(msg.Trim()))
            {
                sb.Append(": ");
                sb.Append(msg);
            }
            int i = 0;
            foreach (IConnection c in conns)
            {
                if (c == conn) continue; //Don't kill ourselves. Ever.
                if (!CheckKill(conn, c)) continue; //Can't kill this user, for whatever reason.
                OnKilledConnection(c);
                c.Drop(sb.ToString());
                i++;
            }
            ((IBackend)this).LogMessage(string.Format("{0} killed {1} connections for namespace {2}", conn.CurrentUser.getInfo("username"),
                i, p.param));
        }

        protected virtual void OnKilledConnection(IConnection c)
        {
            
        }

        private bool CheckKill(IConnection conn, IConnection target)
        {
            int sgpcid = int.Parse(conn.CurrentUser.getInfo("gpc_id"));
            int tgpcid = int.Parse(target.CurrentUser.getInfo("gpc_id"));
            if (sgpcid < tgpcid) return false;
            return true;
        }
        #endregion



        long IBackend.TotalClients
        {
            get { return mTotalConnections; }
        }
    }
}
